package byx.container.core;

import byx.container.core.exception.CyclicDependencyException;
import byx.container.core.utils.CastUtils;
import byx.container.core.utils.ReflectUtils;

import java.util.Arrays;
import java.util.List;
import java.util.function.*;
import java.util.stream.Collectors;

public class ObjectFactories {
    private static <T> ObjectFactory<T> createFactory(ObjectFactory<T> factory, Supplier<String> description) {
        return new ObjectFactory<>() {
            @Override
            public T create() {
                return factory.create();
            }

            @Override
            public String toString() {
                return description.get();
            }
        };
    }

    public static <T> ObjectFactory<T> value(T val) {
        return createFactory(
                () -> val,
                () -> String.format("value(%s)", val)
        );
    }

    public static <T> ObjectFactory<T> ctor(Class<T> type, ObjectFactory<?>... params) {
        return createFactory(
                () -> ReflectUtils.create(type, Arrays.stream(params).map(ObjectFactory::create).toArray()),
                () -> String.format("ctor(%s, %s)", type.getName(), Arrays.toString(params))
        );
    }

    public static <T> ObjectFactory<T> singleton(ObjectFactory<T> factory) {
        final Object[] cache = new Object[]{null};
        return createFactory(
                () -> {
                    if (cache[0] == null) {
                        synchronized (cache) {
                            if (cache[0] == null) {
                                cache[0] = factory.create();
                            }
                        }
                    }
                    return CastUtils.cast(cache[0]);
                },
                () -> String.format("%s.singleton()", factory)
        );
    }

    public static <T> ObjectFactory<T> fromContainer(Container container, Predicate<RegisterInfo> predicate) {
        return createFactory(
                () -> container.getObject(predicate),
                () -> String.format("fromContainer(%s)", predicate)
        );
    }

    public static <T> ObjectFactory<T> fromContainer(Container container, String id) {
        return createFactory(
                () -> container.getObject(id),
                () -> String.format("fromContainer(%s)", id)
        );
    }

    public static <T> ObjectFactory<T> fromContainer(Container container, Class<T> type) {
        return createFactory(
                () -> container.getObject(type),
                () -> String.format("fromContainer(%s)", type.getName())
        );
    }

    public static ObjectFactory<List<?>> allFromContainer(Container container, Predicate<RegisterInfo> predicate) {
        return createFactory(
                () -> container.getObjects(predicate),
                () -> String.format("allFromContainer(%s)", predicate)
        );
    }

    public static <T> ObjectFactory<List<T>> allFromContainer(Container container, Class<T> type) {
        return createFactory(
                () -> container.getObjects(type),
                () -> String.format("allFromContainer(%s)", type.getName())
        );
    }

    public static <T> ObjectFactory<T> call(ObjectFactory<?> instance, String method, ObjectFactory<?>... params) {
        return createFactory(
                () -> {
                    Object obj = instance.create();
                    Object[] objs = Arrays.stream(params).map(ObjectFactory::create).toArray();
                    return ReflectUtils.call(obj, method, objs);
                },
                () -> String.format("%s.call(%s, %s)", instance, method, Arrays.toString(params))
        );
    }

    public static <T, U> ObjectFactory<U> map(ObjectFactory<T> factory, Function<T, U> mapper) {
        return createFactory(
                () -> mapper.apply(factory.create()),
                () -> String.format("%s.map(%s)", factory, mapper)
        );
    }

    public static <T> ObjectFactory<T> init(ObjectFactory<T> factory, Consumer<T> handler) {
        return createFactory(
                map(factory, obj -> {
                    handler.accept(obj);
                    return obj;
                }),
                () -> String.format("%s.init(%s)", factory, handler)
        );
    }

    public static <T> ObjectFactory<T> invoke(ObjectFactory<T> factory, String method, ObjectFactory<?>... params) {
        return createFactory(
                init(factory, obj -> {
                    Object[] objs = Arrays.stream(params).map(ObjectFactory::create).toArray();
                    ReflectUtils.call(obj, method, objs);
                }),
                () -> String.format("%s.invoke(%s, %s)", factory, method, Arrays.toString(params))
        );
    }

    @SafeVarargs
    public static <T> ObjectFactory<List<T>> all(ObjectFactory<T>... factories) {
        return all(List.of(factories));
    }

    public static <T> ObjectFactory<List<T>> all(List<ObjectFactory<T>> factories) {
        return createFactory(
                () -> factories.stream()
                        .map(ObjectFactory::create)
                        .collect(Collectors.toList()),
                () -> String.format("all(%s)", factories)
        );
    }

    public static <T, U> ObjectFactory<U> bind(ObjectFactory<T> factory, Function<T, ObjectFactory<U>> binder) {
        return createFactory(
                () -> binder.apply(factory.create()).create(),
                () -> String.format("%s.bind(%s)", factory, binder)
        );
    }

    public static <T> ObjectFactory<T> guard(ObjectFactory<T> factory) {
        ThreadLocal<Object> flag = new ThreadLocal<>();
        return createFactory(
                () -> {
                    if (flag.get() != null) {
                        throw new CyclicDependencyException();
                    }

                    flag.set(new Object());

                    try {
                        return factory.create();
                    } finally {
                        flag.remove();
                    }
                },
                () -> String.format("%s.guard()", factory)
        );
    }
}
